API Reference
**Referenced Files in This Document** - [worker.js](file://worker.js) - [wrangler.jsonc](file://wrangler.jsonc) - [package.json](file://package.json) - [README.md](file://README.md) - [cloudflare-pages.toml](file://cloudflare-pages.toml) - [src/alliance-login.njk](file://src/alliance-login.njk) - [src/alliance-members.njk](file://src/alliance-members.njk) - [src/feed.njk](file://src/feed.njk) - [src/_data/site.json](file://src/_data/site.json)Table of Contents
- Introduction
- Project Structure
- Core Components
- Architecture Overview
- Detailed Component Analysis
- Dependency Analysis
- Performance Considerations
- Troubleshooting Guide
- Conclusion
- Appendices
Introduction
This document describes the Cloudflare Workers-based APIs and integrations powering the Invest Australia Alliance member portal, live polling data, CMS authentication, and RSS feed generation. It covers endpoint definitions, request/response schemas, authentication, error handling, rate limiting posture, security considerations, and operational guidance.
Project Structure
The platform is built with:
- Eleventy static site generation producing the
_sitedirectory - A Cloudflare Worker that intercepts API and auth routes and serves static assets otherwise
- Wrangler configuration binding assets and KV namespaces
- Nunjucks templates for member-facing pages and RSS feed generation
graph TB
Client["Browser / Client Apps"] --> Worker["Cloudflare Worker<br/>worker.js"]
Worker --> |Intercepts| AuthRoutes["Auth Routes<br/>/alliance/login/, /alliance/verify/, /alliance/logout/"]
Worker --> |Intercepts| PollingAPI["Polling API<br/>/api/polling.json"]
Worker --> |Intercepts| CMSAuth["CMS Auth (Legacy)<br/>/api/auth*, OPTIONS"]
Worker --> Static["Static Assets<br/>_site/ via ASSETS binding"]
AuthRoutes --> Resend["Resend Email Service"]
PollingAPI --> Sheets["Google Sheets API"]
Diagram sources
- [worker.js:297-320](file://worker.js#L297-L320)
- [wrangler.jsonc:9-12](file://wrangler.jsonc#L9-L12)
Section sources
- [worker.js:297-320](file://worker.js#L297-L320)
- [wrangler.jsonc:1-35](file://wrangler.jsonc#L1-L35)
- [package.json:14-13](file://package.json#L14-L13)
Core Components
- Member authentication API: magic-link login, verification, logout, and session management
- Live polling data API: reads from Google Sheets and returns standardized JSON
- CMS integration endpoints: legacy GitHub OAuth for Sveltia CMS
- RSS feed: Atom XML generated from Eleventy collections
Section sources
- [worker.js:77-91](file://worker.js#L77-L91)
- [worker.js:230-276](file://worker.js#L230-L276)
- [worker.js:180-227](file://worker.js#L180-L227)
- [src/feed.njk:1-27](file://src/feed.njk#L1-L27)
Architecture Overview
The Worker acts as a router:
- Auth routes: validate session cookies, issue magic links, verify tokens, and manage logout
- Polling route: fetches live data from Google Sheets and returns normalized JSON
- CMS auth routes: legacy GitHub OAuth handshake for Sveltia CMS
- Static fallback: all other requests are served via the ASSETS binding
sequenceDiagram
participant C as "Client"
participant W as "Worker"
participant KV as "KV Namespaces"
participant R as "Resend"
participant G as "Google Sheets"
rect rgb(255,255,255)
Note over C,W : Member Authentication Flow
C->>W : POST /alliance/login/ {email, _gotcha}
W->>KV : Lookup member : <email>
W->>R : Send magic link email
W-->>C : Redirect /alliance/login/?sent=1
C->>W : GET /alliance/verify/?token=<hex>
W->>KV : Get token : <hex>
W->>KV : Delete token : <hex>
W-->>C : 302 to /alliance/members/ with session cookie
end
rect rgb(255,255,255)
Note over C,W : Polling Data Flow
C->>W : GET /api/polling.json?state=sa|federal
W->>G : Fetch spreadsheet range
G-->>W : Values array
W-->>C : JSON {state, ...polling metrics}
end
Diagram sources
- [worker.js:97-147](file://worker.js#L97-L147)
- [worker.js:153-177](file://worker.js#L153-L177)
- [worker.js:233-276](file://worker.js#L233-L276)
Detailed Component Analysis
Member Authentication API
Endpoints
-
POST /alliance/login/
- Purpose: Issue a one-time magic link email to approved members
- Method: POST
- Body: form-encoded fields
- email: string (required)
- _gotcha: string (honeypot; bot trap)
- Response: 302 redirect to /alliance/login/?sent=1 or error query param
- Security: constant-time email existence checks; always shows “check your email” message
- Notes: Approved emails are stored in KV under member:
-
GET /alliance/verify/
- Purpose: Validate magic link token and set session cookie
- Query: token=
- Response: 302 to /alliance/members/ with HttpOnly Secure session cookie
- Security: token is single-use and TTL-bound; deleted after verification
-
GET /alliance/logout/
- Purpose: Clear session cookie and redirect to login
- Response: 302 to /alliance/login/ with Max-Age=0 cookie
-
GET /alliance/members/*
- Purpose: Protected route gate; validates session cookie and serves assets
- Behavior: If invalid or missing, redirects to /alliance/login/?next=
Session cookie
- Name: ace_member_session
- Attributes: HttpOnly, Secure, SameSite=Lax, Max-Age=30 days
- Signing: HMAC-SHA256 over base64(payload) where payload = email|expiry
Security and validation
- Input validation: email regex; rejects empty or malformed emails
- Timing-safe verification: constant-time HMAC comparison
- CSRF protection: SameSite=Lax; Secure flag mitigates leakage
- Rate limiting: none implemented in code; rely on external controls
Error handling
- 400: Invalid request (e.g., missing code in legacy OAuth)
- 401: Expired or invalid token during verification
- 500: Internal server errors (e.g., KV unconfigured, network failures)
- 503: Misconfigured services (e.g., missing KV or API keys)
Example usage
- Client posts email to /alliance/login/ and waits for email
- User clicks link to /alliance/verify/?token=...
- On success, client stores session cookie and accesses /alliance/members/*
Section sources
- [worker.js:97-147](file://worker.js#L97-L147)
- [worker.js:153-177](file://worker.js#L153-L177)
- [worker.js:282-295](file://worker.js#L282-L295)
- [worker.js:81-91](file://worker.js#L81-L91)
- [worker.js:32-58](file://worker.js#L32-L58)
- [src/alliance-login.njk:21-34](file://src/alliance-login.njk#L21-L34)
- [src/alliance-login.njk:65-71](file://src/alliance-login.njk#L65-L71)
- [src/alliance-members.njk:14](file://src/alliance-members.njk#L14)
Polling Data API
Endpoint
- GET /api/polling.json
- Purpose: Return live polling snapshot for South Australia or Federal
- Query parameters:
- state: sa or federal (default sa)
- Response: JSON object with normalized fields
- state: "sa" or "federal"
- labor_tpp, liberal_tpp, labor_primary, liberal_primary, greens_primary: numbers
- sample_size: integer
- margin_of_error: number
- last_updated: date string
- _cached_at: ISO timestamp
- Headers: Content-Type application/json, Access-Control-Allow-Origin acestrategies.au, Cache-Control public, max-age=300, stale-while-revalidate=3600
- Errors: 500 with JSON { error, message } if fetch fails; 503 if service misconfigured
Integration notes
- Back-end fetches from Google Sheets using document ID and API key from secrets
- Uses a fixed range per state; defaults to SA if state=federal is not provided
Section sources
- [worker.js:233-276](file://worker.js#L233-L276)
- [README.md:507-508](file://README.md#L507-L508)
CMS Integration APIs (Legacy)
Endpoints
-
OPTIONS *
- Purpose: CORS preflight for cross-origin requests from CMS
- Response: Access-Control-Allow-Origin acestrategies.au, methods GET/POST/OPTIONS, headers Content-Type
-
GET /api/auth
- Purpose: Start GitHub OAuth flow for legacy CMS
- Query: none
- Response: 302 redirect to GitHub authorize endpoint with configured client_id and redirect_uri
-
GET /api/auth/callback
- Purpose: Receive OAuth code and return token to opener window
- Query: code (authorization code)
- Response: HTML page that postsMessage the access token to the opener origin
Security and compatibility
- These endpoints are legacy and not required for Sveltia CMS
- CORS is restricted to acestrategies.au origin
- Token is delivered via postMessage to the opener window
Section sources
- [worker.js:183-191](file://worker.js#L183-L191)
- [worker.js:193-198](file://worker.js#L193-L198)
- [worker.js:200-227](file://worker.js#L200-L227)
RSS Feed Generation
Endpoint
- GET /feed.xml
- Purpose: Atom XML feed of news articles
- Source: Eleventy collections.news
- Output: Atom feed with entries containing title, link, updated, id, and content
Section sources
- [src/feed.njk:1-27](file://src/feed.njk#L1-L27)
- [src/_data/site.json:1-20](file://src/_data/site.json#L1-L20)
Dependency Analysis
- Worker depends on:
- ASSETS binding for static file serving
- KV namespaces MEMBER_EMAILS and MAGIC_TOKENS for auth
- Secrets: SESSION_SECRET, RESEND_API_KEY, GOOGLE_SHEETS_ID, GOOGLE_SHEETS_API_KEY, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET
- External services:
- Resend for transactional emails
- Google Sheets for polling data
- GitHub OAuth for legacy CMS integration
graph LR
W["worker.js"] --> A["ASSETS binding"]
W --> ME["KV: MEMBER_EMAILS"]
W --> MT["KV: MAGIC_TOKENS"]
W --> RS["Resend API"]
W --> GS["Google Sheets API"]
W --> GH["GitHub OAuth"]
Diagram sources
- [wrangler.jsonc:9-12](file://wrangler.jsonc#L9-L12)
- [worker.js:98-147](file://worker.js#L98-L147)
- [worker.js:233-276](file://worker.js#L233-L276)
- [worker.js:193-227](file://worker.js#L193-L227)
Section sources
- [wrangler.jsonc:17-34](file://wrangler.jsonc#L17-L34)
- [worker.js:98-147](file://worker.js#L98-L147)
- [worker.js:233-276](file://worker.js#L233-L276)
- [worker.js:193-227](file://worker.js#L193-L227)
Performance Considerations
- Polling API caching: public cache of 5 minutes with stale-while-revalidate of 1 hour
- Static asset delivery: via Cloudflare Workers Assets reduces origin load
- No explicit rate limiting in code; consider deploying CDN rate limits or Worker-side quotas for sensitive endpoints
[No sources needed since this section provides general guidance]
Troubleshooting Guide
Common issues and resolutions
-
Member auth KV not configured
- Symptom: 503 response indicating KV namespaces not configured
- Resolution: Create MEMBER_EMAILS and MAGIC_TOKENS KV namespaces and bind them in wrangler.jsonc
-
Missing or invalid secrets
- Symptom: 503 for polling; 500 for auth-related failures
- Resolution: Ensure SESSION_SECRET, RESEND_API_KEY, GOOGLE_SHEETS_ID, GOOGLE_SHEETS_API_KEY are set
-
Verification token expired or invalid
- Symptom: Redirect to login with error=expired or error=invalid
- Resolution: Request a new magic link; tokens expire after 15 minutes
-
CORS errors for CMS auth
- Symptom: Preflight blocked for /api/auth*
- Resolution: Ensure requests originate from acestrategies.au; verify OPTIONS handling
-
Polling data fetch failure
- Symptom: 500 with error message
- Resolution: Verify GOOGLE_SHEETS_ID and GOOGLE_SHEETS_API_KEY; confirm sheet range availability
Debugging techniques
- Inspect cookies: ace_member_session should be present after successful verification
- Monitor network tab for redirects and headers
- Check Worker logs in Cloudflare dashboard for runtime errors
Section sources
- [worker.js:70-75](file://worker.js#L70-L75)
- [worker.js:244-246](file://worker.js#L244-L246)
- [worker.js:158-159](file://worker.js#L158-L159)
- [worker.js:183-191](file://worker.js#L183-L191)
- [worker.js:270-275](file://worker.js#L270-L275)
Conclusion
The Worker exposes a concise set of authenticated endpoints for member access, a polling data API backed by Google Sheets, and legacy CMS integration hooks. The design emphasizes simplicity, security through signed sessions and constant-time comparisons, and efficient static delivery via Cloudflare Workers Assets.
[No sources needed since this section summarizes without analyzing specific files]
Appendices
Endpoint Reference Summary
-
POST /alliance/login/
- Purpose: Send magic link
- Auth: None
- Response: 302 redirect
- Errors: 400, 500, 503
-
GET /alliance/verify/?token=
- Purpose: Validate token and set session
- Auth: None
- Response: 302 to members portal with session cookie
- Errors: 401, 500
-
GET /alliance/logout/
- Purpose: Clear session
- Auth: None
- Response: 302 to login
- Errors: None
-
GET /alliance/members/*
- Purpose: Protected route gate
- Auth: Cookie required
- Response: Static asset or redirect
- Errors: Redirect to login
-
GET /api/polling.json?state=sa|federal
- Purpose: Live polling snapshot
- Auth: None
- Response: JSON
- Errors: 500, 503
-
OPTIONS *
- Purpose: CORS preflight
- Auth: None
- Response: Headers for acestrategies.au
-
GET /api/auth
- Purpose: Start GitHub OAuth (legacy)
- Auth: None
- Response: 302 redirect
- Errors: 500
-
GET /api/auth/callback
- Purpose: OAuth callback (legacy)
- Auth: None
- Response: HTML with token posted to opener
- Errors: 400, 500
Section sources
- [worker.js:97-147](file://worker.js#L97-L147)
- [worker.js:153-177](file://worker.js#L153-L177)
- [worker.js:282-295](file://worker.js#L282-L295)
- [worker.js:81-91](file://worker.js#L81-L91)
- [worker.js:233-276](file://worker.js#L233-L276)
- [worker.js:183-191](file://worker.js#L183-L191)
- [worker.js:193-227](file://worker.js#L193-L227)
- [README.md:642-652](file://README.md#L642-L652)
Request/Response Schemas
Member login (POST /alliance/login/)
- Request body (form): email (string), _gotcha (string)
- Response: 302 redirect
Verification (GET /alliance/verify/)
- Query: token (string)
- Response: 302 with Set-Cookie: ace_member_session=...; HttpOnly; Secure; SameSite=Lax; Max-Age=...
Polling data (GET /api/polling.json)
- Query: state (sa|federal)
- Response JSON:
- state: "sa"|"federal"
- labor_tpp: number
- liberal_tpp: number
- labor_primary: number
- liberal_primary: number
- greens_primary: number
- sample_size: integer
- margin_of_error: number
- last_updated: string (date)
- _cached_at: string (ISO timestamp)
CMS OAuth (legacy)
- GET /api/auth: 302 to GitHub authorize
- GET /api/auth/callback: HTML with postMessage(token)
RSS feed (GET /feed.xml)
- Response: Atom XML with entries derived from collections.news
Section sources
- [worker.js:97-147](file://worker.js#L97-L147)
- [worker.js:153-177](file://worker.js#L153-L177)
- [worker.js:233-276](file://worker.js#L233-L276)
- [worker.js:193-227](file://worker.js#L193-L227)
- [src/feed.njk:1-27](file://src/feed.njk#L1-L27)
Authentication Requirements
- Member session: HttpOnly Secure cookie named ace_member_session
- Verification: Single-use token bound to KV with TTL
- OAuth (legacy): GitHub OAuth with client credentials
Section sources
- [worker.js:12-14](file://worker.js#L12-L14)
- [worker.js:32-58](file://worker.js#L32-L58)
- [worker.js:153-177](file://worker.js#L153-L177)
- [worker.js:193-227](file://worker.js#L193-L227)
Security Considerations
- CORS: Restricted to acestrategies.au for CMS auth endpoints
- Input validation: email regex; honeypot field for bot detection
- Output sanitization: HTML emails are minimal and static
- Cookie security: HttpOnly, Secure, SameSite=Lax, 30-day Max-Age
- Timing safety: constant-time HMAC verification
- Secrets management: stored via Wrangler secrets; never committed
Section sources
- [worker.js:183-191](file://worker.js#L183-L191)
- [worker.js:111-113](file://worker.js#L111-L113)
- [worker.js:164-171](file://worker.js#L164-L171)
- [worker.js:50-54](file://worker.js#L50-L54)
- [README.md:503-510](file://README.md#L503-L510)
Rate Limiting and Backwards Compatibility
- Rate limiting: not implemented in code; consider upstream controls
- Backwards compatibility: legacy GitHub OAuth endpoints remain for CMS compatibility; primary auth uses session cookies
Section sources
- [worker.js:193-227](file://worker.js#L193-L227)
- [cloudflare-pages.toml:1-4](file://cloudflare-pages.toml#L1-L4)
Versioning Strategy
- The project version is 2.0.0 in package metadata
- No API versioning headers or URLs are used; semantic versioning applies to site and tooling
Section sources
- [package.json:3](file://package.json#L3)